-
-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add 405 handler #65
Add 405 handler #65
Conversation
Pinging @poke to cast his wise eyes over |
src/BotwinExtensions.cs
Outdated
foreach (var module in modules) | ||
{ | ||
foreach (var route in module.Routes.Keys) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We’re already iterating the module routes in line 48, so we could just combine collecting all “system routes” with setting up the normal route handler here, to avoid iterating everything twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially tried to do that but i need to loop to get all routes before applying the 405 handler which is global whereas the route handler is per route
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about something like this:
foreach (var route in …)
{
routeBuilder.MapVerb(…);
knownRoutes.Add(route);
}
builder.UseRouter(routeBuilder.Build());
builder.Use(GetMethodNotAllowedHandler(knownRoutes));
src/BotwinExtensions.cs
Outdated
} | ||
} | ||
|
||
builder.Use(async (context, next) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand it correctly, then this handler is supposed to “pick up” those routes which paths are registered but are using the incorrect verb? In that case, wouldn’t it be better to run this middleware after the normal mapped routes, so it only runs when handling those is over (similarly how in standard ASP.NET, MVC runs after StaticFiles if StaticFiles could not respond to a request).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it runs after, the routing middleware will set 404 as the status code, the issue is, how do you know its a 404 from an unfound route vs a 404 wrongly assigned for invalid verb on path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I’m completely wrong (routing is one of the topics I didn’t really do anything with yet) but from how it looks, the next middleware is only invoked if there is no matching route. So if the middleware is after the router, it will only run for unrecognized routes. So you would only have to check whether any route with such a path exists in order to determine whether it’s a real not found or just an incorrect verb. So you could do this with a simple hash set on the paths.
src/BotwinExtensions.cs
Outdated
? context.Request.Path.Value.Substring(0, context.Request.Path.Value.Length - 1) | ||
: context.Request.Path.Value; | ||
|
||
var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you’re looking up routes by paths, then you shouldn’t use a list. Consider an ILookup<string, string>
instead that maps from paths to the registered verbs.
src/BotwinExtensions.cs
Outdated
: context.Request.Path.Value; | ||
|
||
var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb); | ||
if (verbsForPath.All(x => x != context.Request.Method)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per above: If this would run after the normal handlers as a fallback, then at this point you would be already guaranteed that this is running on an unsupported method. So you wouldn’t actually need to lookup (or even store) the registered methods but just the paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One final question: How should your 405 middleware behave when a module’s route handler deliberately returns a 404 or 405 result on its own? E.g. an API module that will return 404 for a missing resource?
{ | ||
var moduleType = module.GetType(); | ||
|
||
foreach (var route in module.Routes.Keys) | ||
{ | ||
routeBuilder.MapVerb(route.verb, route.path, CreateRouteHandler(route, moduleType)); | ||
|
||
var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what David would say about this (allocations!?!?!), but maybe just route.path.TrimEnd('/')
?
{ | ||
var moduleType = module.GetType(); | ||
|
||
foreach (var route in module.Routes.Keys) | ||
{ | ||
routeBuilder.MapVerb(route.verb, route.path, CreateRouteHandler(route, moduleType)); | ||
|
||
var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path; | ||
systemRoutes.Add((route.verb, "/" + strippedPath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be cheaper to leave off the leading slash here, and trim the leading slash from the current request later instead.
/// <param name="next"></param> | ||
/// <param name="systemRoutes"></param> | ||
/// <returns></returns> | ||
private static async Task GetMethodNotAllowedHandler(HttpContext context, Func<Task> next, IEnumerable<(string verb, string route)> systemRoutes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this does not return a handler function (which is what I was originally suggesting) but is the handler, the Get
feels out of place here. HandleMethodNotAllowed
? Or maybe you should just move this out to a custom middleware class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah tried your approach but no joy
private static async Task GetMethodNotAllowedHandler(HttpContext context, Func<Task> next, IEnumerable<(string verb, string route)> systemRoutes) | ||
{ | ||
//Call the final pipeline which gets ASP.Net Core status code, usually a 404 in this case | ||
await next(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you continuing the pipeline here at the beginning? Shouldn’t this handler already be kind of the final one in the pipeline? Do you need to know that it produced a 404? Wouldn’t it be safe to assume that if this handler runs after the routing middleware, then a route was not found? (again: I have no actual idea about the routing middleware)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oddly before that is called the response status code is 200, if you call next
it's then 404 from the router. Not sure how because as you say I'd expect it to be already at the end of the pipeline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think you actually need to check the status code here though. This should not run if the router succeeded in the pipeline step before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly if I remove the next()
call it will always be 200 on a route not defined in Botwin. If I take the GetMethodNotAllowedHandler out completely I get a 404 so I assume there must be one last pipeline called somewhere
: context.Request.Path.Value; | ||
|
||
//ASP.Net Core will set a 405 response to 404. Let's check if it's a valid 404 first otherwise if we know about the route it's most likely a 405 | ||
if (context.Response.StatusCode == 404 && systemRoutes.Any(x => x.route == strippedPath)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use HashSet or Dictionary for constant time lookup. Also, I don’t know if the router middleware is case insensitive itself but if it is, then pure string equality will not work (you will have to use the RouteComparer we came up with earlier).
Fixed in #66 |
Fixes #51